package org.covalence.core.visitor;

import java.util.HashMap;
import java.util.Map;

import org.apache.log4j.Logger;
import org.covalence.core.Constants;
import org.covalence.core.Registry;
import org.covalence.core.Strings;
import org.covalence.core.util.CTable;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodAdapter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

/**
 * Transforms the constructor of any bound class to initialize the fields added
 * by the BoundClassVisitor including the PropertyChangeSupport field.  It also
 * replaces collection objects initialization with Observable collection objects.  
 * A Collection object defined by the user will be wrapped in an Observable 
 * collection object so that it can notify its parent object whenever its 
 * contents change.
 * <p>
 * <code>
 *   private List list = new ArrayList();
 * </code>
 * <p>
 * gets transformed into the byte-code equivalent of
 * <p>
 * <code>
 *   private List list = new org.covalence.util.CList(new ArrayList());
 * </code>
 * <p>
 * The enclosing or wrapping collection object is deduced from the actual type 
 * so that HashMap becomes CMap, HashSet becomes CSet, ArrayList becomes CList, etc.
 * 
 * @author tslusser
 *
 */
public class BoundInitVisitor extends MethodAdapter 
		implements Constants, Opcodes {

	private boolean isCollectionField;
	
	private boolean isObservableField;
	
	private String className;
	
	private Map<String, String> propType = new HashMap<String, String>();
	
	private static Logger log = Logger.getLogger(BoundInitVisitor.class);
	
	public BoundInitVisitor(MethodVisitor mv, String className) {
		
		super(mv);
		this.className = className;
	}

	public void visitInsn(int opcode) {
		
		if (opcode == RETURN) {
			initPropertyChangeSupportField();
			callAddNestedObservable();
		}
		mv.visitInsn(opcode);
	}

	@Override
	public void visitMethodInsn(int opcode, String owner, String name, 
			String desc) {
		
		log.trace("[classname:" + this.className + 
				", activated:" + isCollectionField + 
				", opcode:" + Strings.opcode(opcode) + 
				", owner:" + owner + 
				", name:" + name + 
				", desc:" + desc + "]");
		super.visitMethodInsn(opcode, owner, name, desc);
		if (isCollectionField) {
			String myOwner = CTable.translate(owner);
			mv.visitMethodInsn(INVOKESPECIAL, myOwner, "<init>", 
					"(Ljava/lang/Object;)V");
			
			log.trace("[classname:" + this.className + 
					", collection:" + isCollectionField + 
					", opcode:INVOKESPECIAL" +  
					", owner:" + myOwner + 
					", name:<init>" + 
					", desc:(Ljava/lang/Object;)V]");
		}
	}
	
	
	@Override
	public void visitFieldInsn(int opcode, String owner, String name, 
			String desc) {
		
		log.trace("[classname:" + this.className + 
				", activated:" + isCollectionField +
				", opcode:" + Strings.opcode(opcode) +
				", owner:" + owner +
				", name:" + name +
				", desc:" + desc + "]");
		super.visitFieldInsn(opcode, owner, name, desc);
		
		if (opcode == PUTFIELD && isCollectionField) {
			propType.put(name, desc);
			isCollectionField = false;
		} else if (opcode == PUTFIELD && isObservableField) {
			propType.put(name, desc);
			isObservableField = false;
		}
	}

	@Override
	public void visitTypeInsn(int opcode, String desc) {
		String translated = CTable.translate(desc);
		if (translated != null) {
			isCollectionField = true;
			mv.visitTypeInsn(NEW, translated);
			mv.visitInsn(DUP);
		} else if (Registry.shouldEnhance(
				Strings.descriptorToClassname(desc))) {
			isObservableField = true;
		}
		log.trace("[classname:" + this.className +   
				", observable: " + this.isObservableField +
				", collection: " + this.isCollectionField +
				", opcode:" + Strings.opcode(opcode) + 
				", desc:" + translated == null ? desc : translated + "]");
		super.visitTypeInsn(opcode, desc);
	}
	
	private void initPropertyChangeSupportField() {
		
		mv.visitVarInsn(ALOAD, 0);
		mv.visitTypeInsn(NEW, CPROPERTY_CHANGE_SUPPORT);
		mv.visitInsn(DUP);
		mv.visitVarInsn(ALOAD, 0);
		mv.visitMethodInsn(INVOKESPECIAL, CPROPERTY_CHANGE_SUPPORT, "<init>", 
			"(Ljava/lang/Object;)V");
		mv.visitFieldInsn(PUTFIELD, className, 
			PROPERTY_CHANGE_SUPPORT_FIELD, CPROPERTY_CHANGE_SUPPORT_TYPE);
	}

	private void callAddNestedObservable() {
		
		for (Map.Entry<String, String> each : propType.entrySet()) {
			String name = each.getKey();
			String type = each.getValue();
			
			Label l0 = new Label();
			mv.visitLabel(l0);
			mv.visitVarInsn(ALOAD, 0);
			mv.visitTypeInsn(CHECKCAST, OBSERVABLE);
			mv.visitVarInsn(ALOAD, 0);
			mv.visitFieldInsn(GETFIELD, this.className, name, type);
			mv.visitTypeInsn(CHECKCAST, OBSERVABLE);
			mv.visitLdcInsn(name);
			mv.visitMethodInsn(INVOKEINTERFACE, OBSERVABLE, 
					"addNestedObservable", 
					ADD_NESTED_OBSERVABLE_SIG);
			
			Label l1 = new Label();
			mv.visitLabel(l1);
			mv.visitVarInsn(ALOAD, 0);
			mv.visitFieldInsn(GETFIELD, this.className, name, type);
			mv.visitTypeInsn(CHECKCAST, OBSERVABLE);
			mv.visitVarInsn(ALOAD, 0);
			mv.visitTypeInsn(CHECKCAST, OBSERVABLE);
			mv.visitMethodInsn(INVOKEINTERFACE, OBSERVABLE, "setParent", 
					SET_PARENT_SIG);

		}
	}
}
